package com.example.shop.redis.config;


import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;

@Configuration
//需要get和set方法
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig {

    @Autowired
    private RedisProperties redisProperties;

    /**
     * 这时因为@value是无法解析非基础类型的对象，但由于支持spEl，才勉强能装配一些集合类型的基础对象
     * 所以spring.redis.slaves必须是基础类型，而不能是[] - {}等
     * 这里我们使用其他方式装配
     */
    // @Value("#{'${spring.redis.slaves}'.split(',')}")
    private List<String> slaves;

    @Resource(name = "redisObjectMapper")
    ObjectMapper redisObjectMapper;


    @Bean("redisTemplate")
    public RedisTemplate<String,Object> redisTemplate() throws Exception {
        //主从复制集群配置。这个可以实现故障迁移。
        RedisStaticMasterReplicaConfiguration replicaConfiguration =
                new RedisStaticMasterReplicaConfiguration(redisProperties.getHost(),redisProperties.getPort());
        //添加从服务器。
        slaves.forEach(node ->{
            String[] split = node.split(":");
            String host = split[0].trim();
            Integer port = Integer.valueOf(split[1]);
            replicaConfiguration.addNode(host,port);
        });
        //redis服务器权限密码
        replicaConfiguration.setPassword(redisProperties.getPassword());
        // 根据连接池配置信息，创建连接池
        GenericObjectPoolConfig<Object> poolConfig = genericTPoolConfig(Object.class,redisProperties.getLettuce().getPool());
        // 根据配置信息创建连接工厂
        LettuceConnectionFactory lettuceConnectionFactory = createLettuceConnectionFactory(redisProperties,replicaConfiguration, poolConfig);
        return createRedisTemplate(Object.class,lettuceConnectionFactory);
    }


    /**
     * 配置对象池
     * @param tClass
     * @param poolProperties 连接池的参数信息
     * @param <T> 对象池接收的类型
     * @return 返回通用的对象池。
     */
    <T> GenericObjectPoolConfig<T> genericTPoolConfig(Class<T> tClass, RedisProperties.Pool poolProperties){
        // 连接池配置
        GenericObjectPoolConfig<T> poolConfig = new GenericObjectPoolConfig<>();
        //设置连接池的最大连接数
        poolConfig.setMaxTotal(poolProperties.getMaxActive());
        //设置连接池中的最大空闲连接
        poolConfig.setMaxIdle(poolProperties.getMaxIdle());
        //设置连接池中的最小空闲连接
        poolConfig.setMinIdle(poolProperties.getMinIdle());
        //设置定期检查回收空闲对象。当为正值时，启动空闲对象回收线程，否则不执行空闲对象回收。
        if (poolProperties.getTimeBetweenEvictionRuns() != null) {
            poolConfig.setTimeBetweenEvictionRunsMillis(poolProperties.getTimeBetweenEvictionRuns().toMillis());
        }
        //当连接池用尽后，调用者是否要等待。这个参数和MaxWaitMillis对应,只有为true时，才会生效MaxWaitMillis
        poolConfig.setBlockWhenExhausted(true);
        //设置连接池中最大阻塞等待时间
        if (poolProperties.getMaxWait() != null) {
            poolConfig.setMaxWaitMillis(poolProperties.getMaxWait().toMillis());
        }
        return poolConfig;
    }

    /**
     * 根据redisConfiguration配置信息类、对象池创建LettuceConnectionFactory
     * 创建一个客户端连接配置的建筑类。用于创建客户端连接配置类。而客户端连接配置类是用于创建客户端连接工厂。
     * 而客户端连接工厂是用于创建客户端连接
     * @param redisProperties 配置文件的参数类
     * @param redisConfiguration redis配置信息类
     * @param genericTPoolConfig 对象池
     * @return 返回对应的连接工厂
     */
    LettuceConnectionFactory createLettuceConnectionFactory(RedisProperties redisProperties, RedisConfiguration redisConfiguration, GenericObjectPoolConfig genericTPoolConfig){
//==================================客户端选项配置======================================
        // 开启自适应集群拓扑刷新和周期拓扑刷新
        ClusterTopologyRefreshOptions.Builder clusterOptionsBuilder=ClusterTopologyRefreshOptions.builder();
        RedisProperties.Lettuce.Cluster.Refresh refresh = redisProperties.getLettuce().getCluster().getRefresh();
        // 集群拓扑刷新周期。
        if (refresh.getPeriod() != null) {
            clusterOptionsBuilder.enablePeriodicRefresh(refresh.getPeriod());
        }
        // 是否应该使用使用所有可用刷新触发器的自适应拓扑刷新。自适应刷新不开启,Redis集群变更时将会导致连接异常
        if (refresh.isAdaptive()) {
            clusterOptionsBuilder.enableAllAdaptiveRefreshTriggers();
        }
        // 创建客户端选项
        ClientOptions clientOptions = ClusterClientOptions.builder()
                //配置拓扑刷新选项。
                .topologyRefreshOptions(clusterOptionsBuilder.build())
                .build();
        // 客户端连接配置的建筑类进行客户端连接配置
        // 创建一个客户端连接配置的建筑类。用于创建客户端连接配置类。而客户端连接配置类是用于创建客户端连接工厂
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
        //客户端连接工厂的建筑类的一些配置
        if (redisProperties.isSsl()) {
            builder.useSsl();
        }
        //配置连接超时时间
        if (redisProperties.getTimeout() != null) {
            builder.commandTimeout(redisProperties.getTimeout());
        }
        //配置关闭超时时间
        if (redisProperties.getLettuce() != null) {
            RedisProperties.Lettuce lettuce = redisProperties.getLettuce();
            if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
                builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());
            }
        }
        // 判断客户端名是否不为null而且长度至少为1
        if (StringUtils.hasText(redisProperties.getClientName())) {
            //设置连接的客户端名
            builder.clientName(redisProperties.getClientName());
        }
        //客户端连接工厂的建筑类配置上对象池
        builder.poolConfig(genericTPoolConfig)
                //设置读取形式。即优先从从服务器读取数据，如果没有从服务器，则从主服务器读取数据
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                //配置客户端选项
                .clientOptions(clientOptions);
        //通过客户端连接工厂的建筑类创建一个客户端连接配置对象
        LettuceClientConfiguration clientConfig = builder.build();

        //redis客户端连接工厂的配置
        //通过配置文件、连接池创建一个redis客户端连接工厂
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration,clientConfig);
        //设置参数后的后处理，这是必须的,不能缺少的
        lettuceConnectionFactory.afterPropertiesSet();
        //是否允许多个线程操作共用同一个缓存连接，默认true，false时每个线程操作都将开辟新的连接
        lettuceConnectionFactory.setShareNativeConnection(false);
        // 重置底层共享连接, 在接下来的访问时初始化
        lettuceConnectionFactory.resetConnection();
        return  lettuceConnectionFactory;
    }

    /**
     * 根据redisConnectionFactory连接到的redis服务器，创建一个redisTemplate对象
     * @param redisConnectionFactory
     * @return 返回创建的redisTemplate对象
     */
    <T> RedisTemplate<String,T> createRedisTemplate(Class<T> tClass, RedisConnectionFactory redisConnectionFactory) throws InstantiationException, IllegalAccessException {
        //泛型不一定RedisTemplate要在传入参数的时候指定，也可以返回的时候，根据赋值类型指定
        RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
        //设置redisTemplate的连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //redis事务，我们不采用setEnableTransactionSupport，配置spring事务管理器什么的
        // 我们使用SessionCallback
        //  redisTemplate.setEnableTransactionSupport(false);


        //=================================redis序列化配置=========================================//
        //默认是jdk序列化，可以去RedisSerializer查看所有的序列化类
        //redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer(new SerializingConverter(), new DeserializingConverter()));
        //创建一个redis双向序列化器，用于处理javaBean类型。基本可以处理绝大部的类型，如果不行在添加新的模型到Jackson2JsonRedisSerializer中
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);


//        //定义一个模型
//        SimpleModule localDateModule = new SimpleModule("LocalDateDeserializer",new Version(2,1,4,null,null,null));
//        //指定反序列化器作用的Java类
//        localDateModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(LocalDate.class));
//        //为ObjectMapper添加一个序列化/反序列化的模型
//        objectMapper.registerModule(localDateModule);

        /**
         * 有时候你要读取的JSON数据的属性要多于你的Java对象的属性, 默认情况下Jackson这时会抛出异常, 含义是无法在Java对象中找到未知属性XXX
         * 但是, 我们有时候又需要允许JSON属性多于要产生的Java对象的属性. 比如, 你想从一个REST服务获取JSON数据, 但是它包含的内容远多于你所需要的.
         * 这是, 通过配置Jackson的Feature使能可以让你忽略那些多余的属性
         */

        jacksonSerializer.setObjectMapper(redisObjectMapper);

        //创建一个redis字符串双向序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //redis解析key时，使用string序列化该对象，即转换为字符串
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setDefaultSerializer(stringRedisSerializer);
        //redis解析value值时，使用json序列化该对象。
        //如果是javaBean对象，则使用json双向序列化器,序列化成json字符串
        if(!(tClass.newInstance() instanceof String)){
            redisTemplate.setValueSerializer(jacksonSerializer);
            redisTemplate.setHashValueSerializer(jacksonSerializer);
        }else {
            redisTemplate.setValueSerializer(stringRedisSerializer);
            redisTemplate.setHashValueSerializer(stringRedisSerializer);
        }
        return redisTemplate;
    }



    public void setSlaves(List<String> slaves) {
        this.slaves = slaves;
    }
}
